#include <nds.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <dswifi9.h>

#include <ctime>
#include <string>
#include <cstring>
#include <cmath>

#include "debug.h"
#include "scrobbler.h"
#include "graphics.h"
#include "md5.h"
#include "command.h"
#include "player.h"

extern Graphics graphics;
extern Player player;

//---------------------------------------------------------------------------------
Scrobbler::Scrobbler()
//---------------------------------------------------------------------------------
{
	online = false;
	scrobblerOk = false;
	nowQueueSent = true;
	irqIsRunning = false;
	TIMER2_DATA = TIMER_FREQ_1024(1);
	TIMER2_CR = TIMER_DIV_1024 | TIMER_IRQ_REQ | TIMER_ENABLE;
}

//---------------------------------------------------------------------------------
std::string Scrobbler::getMessage()
//---------------------------------------------------------------------------------
{
	return message;
}

//---------------------------------------------------------------------------------
void Scrobbler::readCfg()
//---------------------------------------------------------------------------------
{
	// read server data		
	std::string temp;
	fileHandler.open("scrobbler.txt", std::fstream::in);
	if (fileHandler.is_open())
	{
		std::getline(fileHandler, temp);
		if (temp.size() == 0)
		{
			fileHandler.close();
			message = "No host info";
			return;
		}
		serverURL = urlParser(temp);
		std::getline(fileHandler, username);
		if (username.size() == 0)
		{
			fileHandler.close();
			message = "No username";
			return;
		}
		std::getline(fileHandler, password);
		if (password.size() == 0)
		{
			fileHandler.close();
			message = "No password";
			return;
		}
		fileHandler.close();
		message = "Scrobbler offline";
		scrobblerOk = true;
	}
}

//---------------------------------------------------------------------------------
bool Scrobbler::toggleOnline()
//---------------------------------------------------------------------------------
{
	if (!scrobblerOk) return false;
	online = !online;
	return true;
}

//---------------------------------------------------------------------------------
void Scrobbler::readScrobbls()
//---------------------------------------------------------------------------------
{
	fileHandler.open("scrobbls.wei", std::fstream::in);
	std::string filedata;
	scrobbl infodata;
	int tmp;
	
	while ( !fileHandler.eof() )
	{
		std::getline( fileHandler, filedata );
		if (filedata.size() < 12) break;
		
		infodata.timestamp = *((int*)(&filedata[0]));
		infodata.info.length = *((u32*)(&filedata[4]));
		infodata.info.sampleRate = *((u32*)(&filedata[8]));
		tmp = 12;
		infodata.info.album.assign( &(filedata[tmp]) );
		tmp += infodata.info.album.size() + 1;
		infodata.info.track.assign( &(filedata[tmp]) );
		tmp += infodata.info.track.size() + 1;
		infodata.info.artist.assign( &(filedata[tmp]) );
		tmp += infodata.info.artist.size() + 1;
		infodata.info.title.assign( &(filedata[tmp]) );
		
		submitQueue.push_back(infodata);
	}
	fileHandler.close();
	message = "Scrobbls read";
}

//---------------------------------------------------------------------------------
void Scrobbler::nowplaying(fileInfo *tuneInfo)
//---------------------------------------------------------------------------------
{
	if (tuneInfo == NULL)
	{	
		nowQueueSent = true;
	}
	else
	{
		nowQueue = *tuneInfo;
		nowQueueSent = false;
	}
}

//---------------------------------------------------------------------------------
void Scrobbler::submission(fileInfo *tuneInfo, time_t *timestamp)
//---------------------------------------------------------------------------------
{
	scrobbl tmp;
	tmp.info = *tuneInfo;
	tmp.timestamp = *timestamp;
	submitQueue.push_back(tmp);
}

//---------------------------------------------------------------------------------
void Scrobbler::process()
//---------------------------------------------------------------------------------
{
	if ( irqIsRunning == false)
	{
		message = "Processing...";
		irqIsRunning = true;
		if (online)
		{
			nowProcess();
			submitProcess();
			Wifi_DisableWifi();
		}
		#ifndef DEBUG
		graphics.setWiFi(0);
		#endif
		writeScrobbls();
		irqIsRunning = false;
		message = "Processed";
	}
}

//---------------------------------------------------------------------------------
void Scrobbler::writeScrobbls()
//---------------------------------------------------------------------------------
{
	if ( irqIsRunning == false)
	{
		irqIsRunning = true;

		irqDisable(IRQ_VBLANK);
		/* pause the player in order to not read from
		 * the audio file while we are writing scrobbls */
		player.togglepause();
		
		fileHandler.open("scrobbls.wei", std::fstream::out | std::fstream::trunc);
		for (u16 i=0; i<submitQueue.size(); ++i)
		{
			fileHandler.write( (char *)(&submitQueue[i].timestamp), 4 );
			fileHandler.write( (char *)(&submitQueue[i].info.length), 4 );
			fileHandler.write( (char *)(&submitQueue[i].info.sampleRate), 4 );
			fileHandler << submitQueue[i].info.album << std::ends
							<< submitQueue[i].info.track << std::ends
							<< submitQueue[i].info.artist << std::ends
							<< submitQueue[i].info.title << std::endl;
		}
		fileHandler.close();
		
		player.togglepause();
		irqEnable(IRQ_VBLANK);

		irqIsRunning = false;
	}
}

//---------------------------------------------------------------------------------
void Scrobbler::nowProcess()
//---------------------------------------------------------------------------------
{
	if (nowQueueSent) return;
	nowQueueSent = true;
	
	#ifdef DEBUG_SCROBBLER
	iprintf("nowplaying()\n");
	#endif
	if (sessionID.size() == 0)
	{
		if ( !handshake() )
		{
			return;
		}
	}
	
	fileInfo info = nowQueue;
	
	// connect
	int socket = establish(&nowURL);
	if ( socket != SOCKET_ERROR )
	{
		// send request
		std::string request = makeNowRequest(info);
		std::string response = sendrecv(socket, request);
		
		// see if everything's ok
		if ( response.substr( response.find("\r\n\r\n")+4 ).compare("OK\n") != 0 )
		{
			// something's wrong, let's handshake again
			if ( !handshake() )
			{
				return;
			}
			
			// and try once again
			if ( establish(&nowURL) ) {
				response = sendrecv(socket, request);
			} else {
				return;
			}
		}
	}
	#ifdef DEBUG_SCROBBLER
	iprintf("nowplaying():ok\n");
	#endif
}


//---------------------------------------------------------------------------------
void Scrobbler::submitProcess()
//---------------------------------------------------------------------------------
{
	if (submitQueue.empty() == true) return;
	
	if (sessionID.size() == 0)
	{
		if ( !handshake() )
		{
			return;
		}
	}
	
	// connect
	int socket = establish(&submitURL);
	if ( socket != SOCKET_ERROR )
	{
		// send request
		std::string request = makeSubmitRequest();
		std::string response = sendrecv(socket, request);
		
		// see if everything's ok
		if ( response.substr( response.find("\r\n\r\n")+4 ).compare("OK\n") != 0 )
		{
			// something's wrong, let's handshake again
			if ( !handshake() )
			{
				return;
			}
			
			// and try once again
			if ( establish(&submitURL) ) {
				response = sendrecv(socket, request);
				if ( response.substr( response.find("\r\n\r\n")+4 ).compare("OK\n") == 0 )
				{
					for (u8 i=0; i<50; ++i) {
						submitQueue.pop_front();
						if (submitQueue.empty()) break;
					}
				}
			} else {
				return;
			}
		} else {
			for (u8 i=0; i<50; ++i) {
				submitQueue.pop_front();
				if (submitQueue.empty()) break;
			}
		}
	}
}


//---------------------------------------------------------------------------------
bool Scrobbler::handshake()
//---------------------------------------------------------------------------------
{
	#ifdef DEBUG_SCROBBLER
	iprintf("handshake()\n");
	#endif
	
	// connect
	int socket = establish(&serverURL);
	if ( socket == SOCKET_ERROR ) return false;
	
	// make the request
	std::string request = makeHandshakeRequest();
	
	// send the query
	std::string response = sendrecv(socket, request);
	
	// parse the response
	size_t start = response.find("\r\n\r\n", 0);
	
	#ifdef DEBUG_SCROBBLER
	iprintf("handshake():data:\n%s", response.substr(start+4).data());
	#endif
	
	size_t end   = response.find("\n", start+4);

	std::string tmp = "OK";
	start = start+4;
	
	if (response.substr(start, end-start) == tmp) {
		// get session ID
		start = end+1;
		end = response.find('\n', start);
		sessionID = response.substr(start, end-start);
		// get now playing URL
		start = end+1;
		end = response.find('\n', start);
		std::string now = response.substr(start, end-start);
		// get submission URL
		start = end+1;
		end = response.find('\n', start);
		std::string submit = response.substr(start, end-start);
		
		// parse the URLs
		nowURL = urlParser(now);
		submitURL = urlParser(submit);
	} else {
		#ifdef DEBUG_SCROBBLER
		iprintf("handshake():failed\n");
		#endif
		return false;
	}
	#ifdef DEBUG_SCROBBLER
	iprintf("handshake():ok\n");
	#endif
	return true;
}

// *************** *
// WI-FI FUNCTIONS *
// *************** *

//---------------------------------------------------------------------------------
bool Scrobbler::connecTry()
//---------------------------------------------------------------------------------
{
	#ifndef DEBUG
	graphics.setWiFi(1);
	#endif
	
	if (Wifi_CheckInit() == 0) {
		Wifi_InitDefault(INIT_ONLY);
		Wifi_ScanMode();
	}
	else Wifi_EnableWifi();
	
	Wifi_AutoConnect();
	swiIntrWait(1, IRQ_TIMER2);
	
	int status = Wifi_AssocStatus();
	
	u8 counter = 0;
	
	while ( !(status == ASSOCSTATUS_ASSOCIATED || status == ASSOCSTATUS_CANNOTCONNECT) )
	{
		swiIntrWait(1, IRQ_TIMER2);
		status = Wifi_AssocStatus();
		#ifdef DEBUG_SCROBBLER
		iprintf( "\n%s ", ASSOCSTATUS_STRINGS[ Wifi_AssocStatus() ] );
		#endif
		if (counter == 8 && status == ASSOCSTATUS_SEARCHING) {
			return false;
		}
		++counter;
	}
	
	if ( Wifi_AssocStatus() == ASSOCSTATUS_ASSOCIATED )
	{
		#ifndef DEBUG
		graphics.setWiFi(5);
		#endif
		return true;
	}
	
	return false;
}

//---------------------------------------------------------------------------------
int Scrobbler::establish(url* server)
//---------------------------------------------------------------------------------
{
	//try to establish connection to an AP
	if ( !connecTry() ) {
		return SOCKET_ERROR;
	}
	
	// Get IP
	#ifdef DEBUG_SCROBBLER
	iprintf("establish():getIP:%s\n", server->host.data());
	#endif
	struct hostent * myhost = gethostbyname( server->host.data() );
	server->hostIP = *( (unsigned long *)(myhost->h_addr_list[0]) );
	
	if (server->hostIP == 0) {
		#ifdef DEBUG_SCROBBLER
		iprintf("establish():getIP:failed\n");
		#endif
		return SOCKET_ERROR;
	}
	#ifdef DEBUG_SCROBBLER
	iprintf("establish():getIP:%X\n", server->hostIP);
	#endif
	
	int my_socket = socket(AF_INET, SOCK_STREAM, 0);
	
	// Tell the socket to connect to the first IP address we found, on port 80 (HTTP)
	struct sockaddr_in sain;
	sain.sin_family = AF_INET;
	sain.sin_port = htons(server->port);
	sain.sin_addr.s_addr = server->hostIP;
	
	#ifdef DEBUG_SCROBBLER
	iprintf("establish():connect to %s (0x%X)\n", server->host.data(), server->hostIP);
	#endif
	
	if ( connect( my_socket,(struct sockaddr *)&sain, sizeof(sain) ) == -1 ) {
		#ifdef DEBUG_SCROBBLER
		iprintf("establish():connect() failed 1\n");
		#endif
		swiWaitForVBlank();
			if ( connect( my_socket,(struct sockaddr *)&sain, sizeof(sain) ) == -1 ) {
		#ifdef DEBUG_SCROBBLER
			iprintf("establish():connect() failed 2\n");
			#endif
			return SOCKET_ERROR;
		}
	}
	
	#ifdef DEBUG_SCROBBLER
	iprintf("establish():ok\n");
	#endif
	return my_socket;
}

//---------------------------------------------------------------------------------
std::string Scrobbler::sendrecv(int my_socket, std::string request)
//---------------------------------------------------------------------------------
{
	// send our request
	#ifdef DEBUG_SCROBBLER
	iprintf("sendrecv():send\n");
	#endif
	send(my_socket, request.data(), request.length(), 0 );
	
	int recvd_len;
	char incoming_buffer[256];
	std::string response;
	
	#ifdef DEBUG_SCROBBLER
	iprintf("sendrecv():recv\n");
	#endif
	while( ( recvd_len = recv( my_socket, incoming_buffer, 255, 0 ) ) != 0 ) { // if recv returns 0, the socket has been closed.
        if(recvd_len>0) { // data was received!
            incoming_buffer[recvd_len] = 0; // null-terminate
            response.append(incoming_buffer);
		}
	}
	
	closesocket(my_socket);
	
	return response;
}

// ****************** *
// * REQUEST BUILDING *
// * **************** *

//---------------------------------------------------------------------------------
std::string Scrobbler::makeHandshakeRequest()
//---------------------------------------------------------------------------------
{
	// get timestamp
	time_t unixTime = time(NULL);
	
	// convert timestamp to chars
	char timeChar[12];
	timeChar[11] = 0;
	sprintf(timeChar, "%d", (int)unixTime);
	
	#ifdef DEBUG_SCROBBLER
	iprintf("time:%s (%d)\n", timeChar, unixTime);
	#endif
	
	// make the MD5 class
	MD5 md5;
	
	// calculate and store raw md5
	unsigned char *md5sum;
	md5sum = md5.hash((unsigned char*)password.data(), password.length());
	
	// convert md5 to chars
	char md5char[33];
	for (int i=0; i<16; ++i) {
		sprintf(&md5char[i<<1], "%02x", md5sum[i]);
	}
	md5char[32] = 0;
	
	// create the auth string to md5 it
	std::string auth;
	auth.append(md5char);
	auth.append(timeChar);

	// calculate raw md5 of the auth string
	md5sum = md5.hash((unsigned char*)auth.data(), auth.length());
	
	// convert the auth md5 to chars
	for (int i=0; i<16; ++i) {
		sprintf(&md5char[i<<1], "%02x", md5sum[i]);
	}
	md5char[32] = 0;

    // create the request HTTP query
	std::string request;
	request += "GET /?hs=true&p=1.2.1&c=tst&v=1.0&u=";
	request += username;
	request += "&t=";
	request += timeChar;
	request += "&a=";
	request += md5char;
	request += " HTTP/1.1\r\nHost: ";
	request += serverURL.host;
	request += "\r\nUser-Agent: Nintendo DS\r\n\r\n";
	
	return request;
}

//---------------------------------------------------------------------------------
std::string Scrobbler::makeNowRequest(fileInfo tuneInfo)
//---------------------------------------------------------------------------------
{
    // create the request HTTP query
	std::string request;
	request += "POST ";
	request += nowURL.path;
	request += " HTTP/1.1\r\nHost: ";
	request += nowURL.host;
	request += "\r\nUser-Agent: Nintendo DS\r\nContent-Type: application/x-www-form-urlencoded\r\n";
	
	std::string post;
	
	// session ID
	post += "s=" + sessionID;
	
	// artist
	std::string tmp = urlencode((unsigned char*)tuneInfo.artist.data());
	post += "&a=" + tmp;
	
	// track
	tmp = urlencode((unsigned char*)tuneInfo.title.data());
	post += "&t=" + tmp;
	
	// album
	tmp = urlencode((unsigned char*)tuneInfo.album.data());
	post += "&b=" + tmp;
	
	// length
	post += "&l=";
	char temp[10];
	for (u8 i=0; i<10; ++i) temp[i] = 0;
	sprintf(temp, "%d", (int)floor(tuneInfo.length/tuneInfo.sampleRate));
	post.append(temp);
	
	// track number
	tmp = urlencode((unsigned char*)tuneInfo.album.data());
	post += "&n=" + tmp;
	post += "&m=";
	
	#ifdef DEBUG_SCROBBLER
	iprintf("post:%s", post.data());
	#endif
	
	for (u8 i=0; i<10; ++i) temp[i] = 0;
	sprintf(temp, "%d", post.length());
	
	request += "Content-Length: ";
	request.append(temp);
	request += "\r\n\r\n" + post;
	return request;
}


//---------------------------------------------------------------------------------
std::string Scrobbler::makeSubmitRequest()
//---------------------------------------------------------------------------------
{
    // create the request HTTP query
	std::string request;
	request += "POST ";
	request += submitURL.path;
	request += " HTTP/1.1\r\nHost: ";
	request += submitURL.host;
	request += "\r\nUser-Agent: Nintendo DS\r\nContent-Type: application/x-www-form-urlencoded\r\n";
	
	// session ID
	std::string post;
	post += "s=" + sessionID;
	
	char iC[3];
	char temp[10];
	
	for (u8 i=0; i<50; ++i)
	{
		for (u8 j=0; j<3; ++j) iC[j] = 0;
		sprintf(iC, "%d", i);
		
		// artist
		std::string tmp = urlencode((unsigned char*)submitQueue[i].info.artist.data());
		post.append("&a[");
		post.append(iC);
		post.append("]=");
		post.append(tmp);
		
		// track
		tmp = urlencode((unsigned char*)submitQueue[i].info.title.data());
		post.append("&t[");
		post.append(iC);
		post.append("]=");
		post.append(tmp);
		
		// time
		char timeChar[12];
		timeChar[11] = 0;
		sprintf(timeChar, "%d", (int)submitQueue[i].timestamp);
		post.append("&i[");
		post.append(iC);
		post.append("]=");
		post.append(timeChar);
		
		// source, rating
		post.append("&o[");
		post.append(iC);
		post.append("]=P&r[");
		post.append(iC);
		post.append("]=");
		
		// album
		tmp = urlencode((unsigned char*)submitQueue[i].info.album.data());
		post.append("&b[");
		post.append(iC);
		post.append("]=");
		post.append(tmp);
		
		// length
		for (u8 j=0; j<10; ++j) temp[j] = 0;
		sprintf(temp, "%d", (int)floor(submitQueue[i].info.length/submitQueue[i].info.sampleRate));
		post.append("&l[");
		post.append(iC);
		post.append("]=");
		post.append(temp);
		
		// track number
		tmp = urlencode((unsigned char*)submitQueue[i].info.track.data());
		post.append("&n[");
		post.append(iC);
		post.append("]=");
		post.append(tmp);
		post.append("&m[");
		post.append(iC);
		post.append("]=");
		
		#ifdef DEBUG_SCROBBLER
		iprintf("[%d/%d]", i, submitQueue.size());
		#endif
		
		if (submitQueue.size() <= (unsigned int)(i+1)) break;
	}
	
	#ifdef DEBUG_SCROBBLER
	iprintf("post:%s", post.data());
	#endif
	
	for (u8 i=0; i<10; ++i) temp[i] = 0;
	sprintf(temp, "%d", post.length());
	
	request += "Content-Length: ";
	request.append(temp);
	request += "\r\n\r\n" + post;
	return request;
}


//---------------------------------------------------------------------------------
url Scrobbler::urlParser(std::string URL)
//---------------------------------------------------------------------------------
{
	url ret;
	size_t start = URL.find(':', 0) + 3;
	size_t end = URL.find(':', start);
	ret.host = URL.substr(start, end-start);
	start = end+1;
	end = URL.find('/', start);
	ret.port = atoi( URL.substr(start, end-start).data() );
	ret.path = URL.substr(end);
	return ret;
}

std::string Scrobbler::urlencode(unsigned char* str)
{
	std::string ret;
	for (u32 i=0; i<strlen((char*)str); ++i) {	
		// reserved characters
		if (str[i] == '!') ret.append("%21");
		else if (str[i] == '*') ret.append("%2A");
		else if (str[i] == '"') ret.append("%22");
		else if (str[i] == '\'') ret.append("%27");
		else if (str[i] == '(') ret.append("%28");
		else if (str[i] == ')') ret.append("%29");
		else if (str[i] == ';') ret.append("%3B");
		else if (str[i] == ':') ret.append("%3A");
		else if (str[i] == '@') ret.append("%40");
		else if (str[i] == '&') ret.append("%26");
		else if (str[i] == '=') ret.append("%3D");
		else if (str[i] == '+') ret.append("%2B");
		else if (str[i] == '$') ret.append("%24");
		else if (str[i] == ',') ret.append("%2C");
		else if (str[i] == '/') ret.append("%2F");
		else if (str[i] == '?') ret.append("%3F");
		else if (str[i] == '%') ret.append("%25");
		else if (str[i] == '#') ret.append("%23");
		else if (str[i] == '[') ret.append("%5B");
		else if (str[i] == ']') ret.append("%5D");
		// outside of unreserved
		else if (
				      (str[i] != '-' && str[i] != '.' && str[i] != '_' && str[i] != '~')
				   && (str[i] > 0x39 || str[i] < 0x30) // 0-9
				   && (str[i] > 0x5A || str[i] < 0x41) // A-Z
				   && (str[i] > 0x7A || str[i] < 0x61) // a-z
				 )
		{
			char tmp[3];
			tmp[2] = 0;
			sprintf(tmp, "%X", str[i]);
			ret.append("%");
			ret.append(tmp);
		} else {
			char tmp[2];
			tmp[0] = str[i];
			tmp[1] = 0;
			ret.append(tmp);
		}
	}
	return ret;
}
